/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.geode.management.internal;

import java.lang.management.ManagementFactory;
import java.lang.reflect.Type;
import java.text.MessageFormat;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import javax.management.AttributeNotFoundException;
import javax.management.InstanceAlreadyExistsException;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanException;
import javax.management.MBeanRegistrationException;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.NotCompliantMBeanException;
import javax.management.NotificationBroadcasterSupport;
import javax.management.NotificationEmitter;
import javax.management.ObjectInstance;
import javax.management.ObjectName;
import javax.management.ReflectionException;

import org.apache.geode.distributed.DistributedMember;
import org.apache.geode.distributed.internal.InternalDistributedSystem;
import org.apache.geode.distributed.internal.membership.InternalDistributedMember;
import org.apache.geode.internal.ClassLoadUtil;
import org.apache.geode.management.AsyncEventQueueMXBean;
import org.apache.geode.management.CacheServerMXBean;
import org.apache.geode.management.DiskStoreMXBean;
import org.apache.geode.management.DistributedLockServiceMXBean;
import org.apache.geode.management.DistributedRegionMXBean;
import org.apache.geode.management.DistributedSystemMXBean;
import org.apache.geode.management.GatewayReceiverMXBean;
import org.apache.geode.management.GatewaySenderMXBean;
import org.apache.geode.management.LocatorMXBean;
import org.apache.geode.management.LockServiceMXBean;
import org.apache.geode.management.ManagementException;
import org.apache.geode.management.ManagerMXBean;
import org.apache.geode.management.MemberMXBean;
import org.apache.geode.management.RegionMXBean;

/**
 * Utility class to interact with the JMX server
 * 
 * 
 */
public class MBeanJMXAdapter implements ManagementConstants {

  /** The <code>MBeanServer</code> for this application */
  public static MBeanServer mbeanServer = ManagementFactory
      .getPlatformMBeanServer();

  /**
	 * 
	 */
  private Map<ObjectName, Object> localGemFireMBean;
  
  private DistributedMember distMember;

  /**
   * log writer, or null if there is no distributed system available
   */
//  private LogWriterI18n logger = InternalDistributedSystem.getLoggerI18n();

  /**
   * public constructor
   */
  
  public MBeanJMXAdapter() {
    this.localGemFireMBean = new ConcurrentHashMap<ObjectName, Object>();
    this.distMember = InternalDistributedSystem.getConnectedInstance()
        .getDistributedMember();  
  }

  /**
   * This method will register an MBean in GemFire domain. Even if the client
   * provides a domain name it will be ignored and GemFire domain name will be
   * used.
   * 
   * This method checks the local Filter for registering the MBean. If filtered
   * the MBean wont be registered. Although the filter will remember the
   * filtered MBean and register it once the filter is removed.
   * 
   * @param object
   * @param objectName
   * @return modifed ObjectName
   */
  public ObjectName registerMBean(Object object, ObjectName objectName, boolean isGemFireMBean) {
    ObjectName newObjectName = objectName;
    try {
      if (!isGemFireMBean) {
        String member = getMemberNameOrId(distMember);
        String objectKeyProperty = objectName.getKeyPropertyListString();
        
        newObjectName = ObjectName.getInstance(OBJECTNAME__PREFIX + objectKeyProperty + 
            KEYVAL_SEPARATOR + "member=" + member);
      }
      mbeanServer.registerMBean(object, newObjectName);
      this.localGemFireMBean.put(newObjectName, object);

    } catch (InstanceAlreadyExistsException e) {
      throw new ManagementException(e);
    } catch (MBeanRegistrationException e) {
      throw new ManagementException(e);
    } catch (NotCompliantMBeanException e) {
      throw new ManagementException(e);
    } catch (MalformedObjectNameException e) {
      throw new ManagementException(e);
    } catch (NullPointerException e) {
      throw new ManagementException(e);
    }
    return newObjectName;
  }

  /**
   * Checks whether an MBean implements notification support classes or not
   * 
   * @param objectName
   * @return if this MBean can be a notification broadcaster
   */
  public boolean hasNotificationSupport(ObjectName objectName) {
    try {
      if(!isRegistered(objectName)){
        return false;
      }
      ObjectInstance instance = mbeanServer.getObjectInstance(objectName);
      String className = instance.getClassName();
      Class cls = ClassLoadUtil.classFromName(className);
      Type[] intfTyps = cls.getGenericInterfaces();
      for (int i = 0; i < intfTyps.length; i++) {
        Class intfTyp = (Class)intfTyps[i];
        if (intfTyp.equals(NotificationEmitter.class)) {
          return true;
        }
      }
      Class supreClassTyp = (Class)cls.getGenericSuperclass();
      if (supreClassTyp != null
          && supreClassTyp.equals(NotificationBroadcasterSupport.class)) {
        return true;
      }
    } catch (InstanceNotFoundException e) {
      throw new ManagementException(e);
    } catch (ClassNotFoundException e) {
      throw new ManagementException(e);
    }
    return false;
  }

  /**
   * This method will register an MBean in GemFire domain. Even if the client
   * provides a domain name it will be ignored and GemFire domain name will be
   * used.
   * 
   * This method checks the local Filter for registering the MBean. If filtered
   * the MBean wont be registered. Although the filter will remember the
   * filtered MBean and register it once the filter is removed.
   * 
   * @param object
   * @param objectName
   */
  public void registerMBeanProxy(Object object, ObjectName objectName) {

    try {

      mbeanServer.registerMBean(object, objectName);

    } catch (InstanceAlreadyExistsException e) {
      throw new ManagementException(e);
    } catch (MBeanRegistrationException e) {
      throw new ManagementException(e);
    } catch (NotCompliantMBeanException e) {
      throw new ManagementException(e);
    } catch (NullPointerException e) {
      throw new ManagementException(e);
    }

  }

  /**
   * 
   * This method will unregister the MBean from GemFire Domain
   * 
   * @param objectName
   */
  public void unregisterMBean(ObjectName objectName) {

    try {

      if(!isRegistered(objectName)){
        return;
      }
      mbeanServer.unregisterMBean(objectName);
      // For Local GemFire MBeans
      
      if (localGemFireMBean.get(objectName) != null) {
        localGemFireMBean.remove(objectName);
      }
    } catch (NullPointerException e) {
      throw new ManagementException(e);
    } catch (InstanceNotFoundException e) {
      throw new ManagementException(e);
    } catch (MBeanRegistrationException e) {
      throw new ManagementException(e);
    }

  }
  
  public Object getMBeanObject(ObjectName objectName){
    return localGemFireMBean.get(objectName);
  }
  
  /**
   * Finds the MBean instance by {@link javax.management.ObjectName}
   * 
   * @param objectName
   * @param interfaceClass
   * @return instance of MBean
   */
  public <T> T findMBeanByName(ObjectName objectName, Class<T> interfaceClass) {

    Object mbeanInstance = localGemFireMBean.get(objectName);
    if (mbeanInstance != null) {
      return interfaceClass.cast(mbeanInstance);
    } else {
      return null;
    }
  }

  public boolean isLocalMBean(ObjectName objectName){
    return localGemFireMBean.containsKey(objectName);
  }
  /**
   * Method to unregister all the local GemFire MBeans
   * 
   */

  public void unregisterAll() {
    try {
      ObjectName name = new ObjectName(OBJECTNAME__PREFIX + "*");
      Set<ObjectName> gemFireObjects = mbeanServer.queryNames(name, null);

      for (ObjectName objectName : gemFireObjects) {
        unregisterMBean(objectName);
      }
    } catch (MalformedObjectNameException e) {
      throw new ManagementException(e);
    } catch (NullPointerException e) {
      throw new ManagementException(e);
    }

  }

  public void cleanJMXResource() {
    localGemFireMBean.clear();
    unregisterAll();
}
  
  public boolean isRegistered(ObjectName objectName){
    
    return mbeanServer.isRegistered(objectName);
  }

  /**
   * This method returns the name that will be used for a DistributedMember when it is registered
   * as a JMX bean.
   * 
   * @param member Member to find the name for
   * @return The name used to register this member as a JMX bean.
   */
  public static String getMemberNameOrId(DistributedMember member) {
    if (member.getName() != null && !member.getName().equals("")) {
      return makeCompliantName(member.getName());
    }
    return makeCompliantName(member.getId());
  }
  
  /**
   * Return a String that been modified to be compliant as a property of an
   * ObjectName.
   * <p>
   * The property name of an ObjectName may not contain any of the following
   * characters: <b><i>: , = * ?</i></b>
   * <p>
   * This method will replace the above non-compliant characters with a dash:
   * <b><i>-</i></b>
   * <p>
   * If value is empty, this method will return the string "nothing".
   * <p>
   * Note: this is <code>public</code> because certain tests call this from
   * outside of the package. 
   * 
   * @param value
   *          the potentially non-compliant ObjectName property
   * @return the value modified to be compliant as an ObjectName property
   */
  public static String makeCompliantName(String value) {
    value = value.replace(':', '-');
    value = value.replace(',', '-');
    value = value.replace('=', '-');
    value = value.replace('*', '-');
    value = value.replace('?', '-');
    if (value.length() < 1) {
      value = "nothing";
    }
    return value;
  }
  
  public static String makeCompliantRegionPath(String value) {
    if (isQuoted(value)) {
      value = ObjectName.unquote(value);
    } else {
      if (containsSpecialChar(value)) {
        value = ObjectName.quote(value);
      }
    }
    return value;
  }

  private static boolean containsSpecialChar(String value) {
    if (value.contains(":")) {
      return true;
    }
    if (value.contains("@")) {
      return true;
    }
    if (value.contains("-")) {
      return true;
    }
    if (value.contains("#")) {
      return true;
    }
    if (value.contains("+")) {
      return true;
    }
    if (value.contains("?")) {
      return true;
    }
    return false;
  }
  
  private static boolean isQuoted(String value){
    final int len = value.length();
    if (len < 2 || value.charAt(0) != '"' || value.charAt(len - 1) != '"'){
      return false;
    }else{
      return true;
    }
  }

  /**
	 * 
	 */

  public static String makeCompliantRegionNameAppender(String value) {

    return value.replace("/", "-").replace(":", "");

  }

  /**
   * returns Member MBean if any
   * 
   */
  public MemberMXBean getMemberMXBean() {
    ObjectName objName = getMemberMBeanName(distMember);
    return (MemberMXBean) localGemFireMBean.get(objName);

  }

  public RegionMXBean getLocalRegionMXBean(String regionPath) {
    ObjectName objName = getRegionMBeanName(distMember, regionPath);
    return (RegionMXBean) localGemFireMBean.get(objName);

  }

  public LockServiceMXBean getLocalLockServiceMXBean(String lockServiceName) {
    ObjectName objName = getLockServiceMBeanName(distMember, lockServiceName);
    return (LockServiceMXBean) localGemFireMBean.get(objName);
  }

  public DiskStoreMXBean getLocalDiskStoreMXBean(String disStoreName) {
    ObjectName objName = getDiskStoreMBeanName(distMember, disStoreName);
    return (DiskStoreMXBean) localGemFireMBean.get(objName);

  }

  public CacheServerMXBean getClientServiceMXBean(int serverPort) {
    ObjectName objName = getClientServiceMBeanName(serverPort,distMember);
    return (CacheServerMXBean) localGemFireMBean.get(objName);

  }

  public DistributedLockServiceMXBean getDistributedLockServiceMXBean(
      String lockServiceName) {
    ObjectName objName = getDistributedLockServiceName(lockServiceName);
    return (DistributedLockServiceMXBean) localGemFireMBean.get(objName);
  }

  public DistributedRegionMXBean getDistributedRegionMXBean(String regionPath) {
    ObjectName objName = getDistributedRegionMbeanName(regionPath);
    return (DistributedRegionMXBean) localGemFireMBean.get(objName);
  }

  public ManagerMXBean getManagerMXBean() {
    ObjectName objName = getManagerName();
    return (ManagerMXBean) localGemFireMBean.get(objName);
  }
  
  public DistributedSystemMXBean getDistributedSystemMXBean() {
    ObjectName objName = getDistributedSystemName();
    return (DistributedSystemMXBean) localGemFireMBean.get(objName);
  }
  
  public GatewayReceiverMXBean getGatewayReceiverMXBean() {
    ObjectName objName = getGatewayReceiverMBeanName(distMember);
    return (GatewayReceiverMXBean) localGemFireMBean.get(objName);
  }

  public GatewaySenderMXBean getGatewaySenderMXBean(String senderId) {
    ObjectName objName = getGatewaySenderMBeanName(distMember, senderId);
    return (GatewaySenderMXBean) localGemFireMBean.get(objName);
  }
  
  public AsyncEventQueueMXBean getAsyncEventQueueMXBean(String queueId) {
    ObjectName objName = getAsycnEventQueueMBeanName(distMember, queueId);
    return (AsyncEventQueueMXBean) localGemFireMBean.get(objName);
  }
  

  public LocatorMXBean getLocatorMXBean() {
    ObjectName objName = getLocatorMBeanName(distMember);
    return (LocatorMXBean) localGemFireMBean.get(objName);
  }

  public static ObjectName getObjectName(String name) {
    try {
      return ObjectName.getInstance(name);
    } catch (MalformedObjectNameException e) {
      throw new ManagementException(e);
    } catch (NullPointerException e) {
      throw new ManagementException(e);
    }
  }

  public static ObjectName getMemberMBeanName(DistributedMember member) {
    return getObjectName((MessageFormat.format(OBJECTNAME__MEMBER_MXBEAN, new Object[] { getMemberNameOrId(member) })));
  }

  public static ObjectName getMemberMBeanName(String member) {
    return getObjectName((MessageFormat.format(OBJECTNAME__MEMBER_MXBEAN, new Object[] { makeCompliantName(member) })));
  }

  public static ObjectName getRegionMBeanName(DistributedMember member,
      String regionPath) {

    return getObjectName((MessageFormat.format(OBJECTNAME__REGION_MXBEAN,
        new Object[] { makeCompliantRegionPath(regionPath),
            getMemberNameOrId(member) })));
  }

  public static ObjectName getRegionMBeanName(String member, String regionPath) {
    return getObjectName((MessageFormat.format(OBJECTNAME__REGION_MXBEAN,
        new Object[] { makeCompliantRegionPath(regionPath),
            makeCompliantName(member) })));
  }

  public static ObjectName getRegionMBeanName(ObjectName memberMBeanName, String regionPath) {
    return getObjectName((MessageFormat.format(OBJECTNAME__REGION_MXBEAN, new Object[] { makeCompliantRegionPath(regionPath),
        memberMBeanName.getKeyProperty(ManagementConstants.OBJECTNAME_MEMBER_APPENDER) })));
  }

  public static ObjectName getDiskStoreMBeanName(DistributedMember member, String diskName) {
    return getObjectName((MessageFormat.format(OBJECTNAME__DISKSTORE_MXBEAN, new Object[] { diskName, getMemberNameOrId(member) })));
  }

  public static ObjectName getDiskStoreMBeanName(String member, String diskName) {
    return getObjectName((MessageFormat.format(OBJECTNAME__DISKSTORE_MXBEAN, new Object[] { diskName, makeCompliantName(member) })));
  }

  public static ObjectName getClientServiceMBeanName(int serverPort, DistributedMember member) {
    return getObjectName((MessageFormat.format(OBJECTNAME__CLIENTSERVICE_MXBEAN, new Object[] { String.valueOf(serverPort),
        getMemberNameOrId(member) })));
  }

  public static ObjectName getClientServiceMBeanName(int serverPort, String member) {
    return getObjectName((MessageFormat.format(OBJECTNAME__CLIENTSERVICE_MXBEAN, new Object[] { String.valueOf(serverPort),
        makeCompliantName(member) })));
  }

  public static ObjectName getLockServiceMBeanName(DistributedMember member, String lockServiceName) {
    return getObjectName((MessageFormat.format(OBJECTNAME__LOCKSERVICE_MXBEAN, new Object[] { lockServiceName,
        getMemberNameOrId(member) })));
  }

  public static ObjectName getLockServiceMBeanName(String member, String lockServiceName) {
    return getObjectName((MessageFormat.format(OBJECTNAME__LOCKSERVICE_MXBEAN, new Object[] { lockServiceName,
        makeCompliantName(member) })));
  }

  public static ObjectName getGatewayReceiverMBeanName(DistributedMember member) {
    return getObjectName((MessageFormat.format(OBJECTNAME__GATEWAYRECEIVER_MXBEAN, new Object[] { getMemberNameOrId(member) })));
  }

  public static ObjectName getGatewayReceiverMBeanName(String member) {
    return getObjectName((MessageFormat.format(OBJECTNAME__GATEWAYRECEIVER_MXBEAN, new Object[] { makeCompliantName(member) })));
  }

  public static ObjectName getGatewaySenderMBeanName(DistributedMember member, String id) {
    return getObjectName((MessageFormat.format(OBJECTNAME__GATEWAYSENDER_MXBEAN, new Object[] { id, getMemberNameOrId(member) })));
  }

  public static ObjectName getGatewaySenderMBeanName(String member, String id) {
    return getObjectName((MessageFormat.format(OBJECTNAME__GATEWAYSENDER_MXBEAN, new Object[] { id, makeCompliantName(member) })));
  }

  public static ObjectName getAsycnEventQueueMBeanName(DistributedMember member, String queueId) {
    return getObjectName((MessageFormat.format(OBJECTNAME__ASYNCEVENTQUEUE_MXBEAN, new Object[] { queueId,
        getMemberNameOrId(member) })));
  }

  public static ObjectName getAsycnEventQueueMBeanName(String member, String queueId) {
    return getObjectName((MessageFormat.format(OBJECTNAME__ASYNCEVENTQUEUE_MXBEAN, new Object[] { queueId,
        makeCompliantName(member) })));
  }

  public static ObjectName getDistributedRegionMbeanName(String regionPath) {
    return getObjectName((MessageFormat.format(OBJECTNAME__DISTRIBUTEDREGION_MXBEAN, new Object[] { makeCompliantRegionPath(regionPath) })));
  }
  
  /**
   * Without special character transformation
   * @param regionPath region path
   * @return ObjectName MBean name
   */
  public static ObjectName getDistributedRegionMbeanNameInternal(String regionPath) {
    return getObjectName((MessageFormat.format(OBJECTNAME__DISTRIBUTEDREGION_MXBEAN, new Object[] { regionPath })));
  }

  public static ObjectName getDistributedLockServiceName(String lockService) {
    return getObjectName((MessageFormat.format(OBJECTNAME__DISTRIBUTEDLOCKSERVICE_MXBEAN, new Object[] { lockService })));
  }

  public static ObjectName getDistributedSystemName() {
    return getObjectName(OBJECTNAME__DISTRIBUTEDSYSTEM_MXBEAN);
  }

  public static ObjectName getManagerName() {
    String member = getMemberNameOrId(InternalDistributedSystem.getConnectedInstance().getDistributedMember());
    return getObjectName((MessageFormat.format(OBJECTNAME__MANAGER_MXBEAN, new Object[] { member })));
  }

  public static ObjectName getLocatorMBeanName(DistributedMember member) {
    return getObjectName((MessageFormat.format(OBJECTNAME__LOCATOR_MXBEAN, new Object[] { getMemberNameOrId(member) })));
  }

  public static ObjectName getLocatorMBeanName(String member) {
    return getObjectName((MessageFormat.format(OBJECTNAME__LOCATOR_MXBEAN, new Object[] { makeCompliantName(member) })));
  }

  public static ObjectName getCacheServiceMBeanName(DistributedMember member, String cacheServiceId) {
    return getObjectName((MessageFormat.format(OBJECTNAME__CACHESERVICE_MXBEAN, new Object[] { cacheServiceId,
        getMemberNameOrId(member) })));
  }

  public Map<ObjectName, Object> getLocalGemFireMBean() {
    return this.localGemFireMBean;
  }

  public static String getUniqueIDForMember(DistributedMember member) {

    InternalDistributedMember iMember = (InternalDistributedMember) member;
    final StringBuilder sb = new StringBuilder();
    sb.append(iMember.getInetAddress().getHostAddress());
    sb.append("<v" + iMember.getVmViewId() + ">"); // View ID will be 0 for
    // Loner, but in that case no
    // federation as well
    sb.append(iMember.getPort());
    return makeCompliantName(sb.toString().toLowerCase());// Lower case to
    // handle IPv6
  }
  
  public static boolean isAttributeAvailable(String attributeName,
      String objectName) {

      try {
        ObjectName objName = new ObjectName(objectName);
        mbeanServer.getAttribute(objName, attributeName);
      } catch (MalformedObjectNameException e) {
        return false;
      } catch (NullPointerException e) {
        return false;
      } catch (AttributeNotFoundException e) {
        return false;
      } catch (InstanceNotFoundException e) {
        return false;
      } catch (MBeanException e) {
        return false;
      } catch (ReflectionException e) {
        return false;
      }

    return true;

  }
  
  public static int VALUE_NOT_AVAILABLE = -1;

}
